//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Xml;

namespace Microsoft.Research.ScientificWorkflow.Helper
{
    /// <summary>
    /// This class builds the DAP URL for execution on the server. 
    /// Using the URL this class downloads the xml schema and maintains
    /// a local copy in a dom object.
    /// </summary>
    public class OpenDAPUrlBuilder
    {
        public OpenDAPUrlBuilder(OpenDAPURI dapURI)
        {
            this.dapURI = dapURI;
            this.metaXml = new XmlDocument();
            typeOperatorsMap = new Dictionary<OpenDAPURI.DapVariableType, List<OpenDAPURI.DAPOperators>>();
            FillTypeOperatorMap();
            Init();
        }

        #region Private Methods

        /// <summary>
        /// This method finds a node in the meta xml from the DAP server.
        /// This method expects the dap type and the variable name as
        /// arguments.
        /// </summary>
        /// <returns>The node fetched else null</returns>
        private XmlNode FindNode(string dapType, string varName)
        {
            System.Diagnostics.Debug.Assert(this.metaXml.ChildNodes.Count == 2);

            // Get the child node collection for the 
            XmlNodeList variableNodes = this.metaXml.ChildNodes[1].ChildNodes;

            foreach (XmlNode node in variableNodes)
            {
                XmlNode attr = node.Attributes.GetNamedItem("name");
                if (null == attr)
                {
                    continue;
                }

                if (node.Name == dapType && attr.Value == varName)
                    return node;
            }
            return null;
        }

        /// <summary>
        /// This method fills the type operator map with operators
        /// supported for types.
        /// </summary>
        private void FillTypeOperatorMap()
        {
            List<OpenDAPURI.DAPOperators> operatorList = new List<OpenDAPURI.DAPOperators>();

            // Add supported operators for Array
            operatorList.Add(OpenDAPURI.DAPOperators.Sample);
            this.typeOperatorsMap.Add(OpenDAPURI.DapVariableType.Array, operatorList);

            // Add supported operators for Grid
            operatorList.Clear();
            operatorList.Add(OpenDAPURI.DAPOperators.Sample);
            this.typeOperatorsMap.Add(OpenDAPURI.DapVariableType.Grid, operatorList);
        }

        /// <summary>
        /// This method does a look up in the type-operator map to find
        /// if the operator type passed is valied for the DAP variable 
        /// type.
        /// </summary>
        private bool IsOperatorValid(OpenDAPURI.DapVariableType dapVarType,
            OpenDAPURI.DAPOperators dapOperator)
        {
            List<OpenDAPURI.DAPOperators> operatorList = this.typeOperatorsMap[dapVarType];

            foreach (OpenDAPURI.DAPOperators op in operatorList)
            {
                if (op == dapOperator)
                    return true;
            }
            return false;
        }

        /// <summary>
        /// This method reads the xml node for the dap type and creates
        /// an object of InternalDAPType to store the layout for the DAP
        /// type.
        /// </summary>
        private InternalDAPType ConstructInternalType(int index, string varName,
            OpenDAPURI.DapVariableType type, XmlNode varNode)
        {
            InternalDAPType internalType = null;
            if (OpenDAPURI.DapVariableType.Float64 == type ||
                OpenDAPURI.DapVariableType.Float32 == type ||
                OpenDAPURI.DapVariableType.Int16 == type ||
                OpenDAPURI.DapVariableType.Int32 == type)
            {
                internalType = new InternalDAPType(varName, type, varNode.Name, varName, 1);
            }
            else if (OpenDAPURI.DapVariableType.Array == type)
            {
                List<string> names = new List<string>();
                string[] types = new string[1];
                List<int> sizes = new List<int>();

                string mainName = varNode.Attributes.GetNamedItem("name").Value;
                foreach (XmlNode node in varNode.ChildNodes)
                {
                    if (node.Name == "Attribute")
                        continue;
                    else if (node.Name == "dimension")
                    {
                        names.Add(node.Attributes.GetNamedItem("name").Value);
                        sizes.Add(Convert.ToInt32(node.Attributes.GetNamedItem("size").Value));
                    }
                    else
                        types[0] = node.Name;
                }
                internalType = new InternalDAPType(mainName, type, types, names.ToArray(), sizes.ToArray());
            }
            else if (OpenDAPURI.DapVariableType.Grid == type)
            {
                List<string> names = new List<string>();
                List<string> types = new List<string>();
                List<int> sizes = new List<int>();

                string mainName = varNode.Attributes.GetNamedItem("name").Value;
                foreach (XmlNode node in varNode.ChildNodes)
                {
                    if (node.Name == "Attribute")
                        continue;
                    else if (node.Name == "Array")
                    {
                        names.Insert(0, node.Attributes.GetNamedItem("name").Value);
                        int size = 1;
                        foreach (XmlNode node1 in node.ChildNodes)
                        {
                            if (node1.Name == "dimension")
                                size = size * Convert.ToInt32(node1.Attributes.GetNamedItem("size").Value);
                            else
                                types.Insert(0, node1.Name);
                        }
                        sizes.Insert(0, size);
                    }
                    else if (node.Name == "Map")
                    {
                        names.Add(node.Attributes.GetNamedItem("name").Value);
                        foreach (XmlNode node1 in node.ChildNodes)
                        {
                            if (node1.Name == "dimension")
                                sizes.Add(Convert.ToInt32(node1.Attributes.GetNamedItem("size").Value));
                            else
                                types.Add(node1.Name);
                        }
                    }
                }
                internalType = new InternalDAPType(mainName, type, types.ToArray(), names.ToArray(), sizes.ToArray());
            }
            return internalType;
        }

        /// <summary>
        /// This method verifies if the variables to be fetched exist in
        /// the file.
        /// </summary>
        /// <returns></returns>
        private bool CheckVariables()
        {
            int variableCount = this.dapURI.VariableCount;
            for (int i = 0; i < variableCount; ++i)
            {
                // Form the XPath query using the varible name
                // and type and then try getting a node. If not found
                // return false.
                string varName; OpenDAPURI.DapVariableType type;
                this.dapURI.GetVariableDetails(i, out varName, out type);

                XmlNode varNode = FindNode(type.ToString(), varName);

                if (null == varNode)
                    return false;

                InternalDAPType dapType = ConstructInternalType(i, varName, type, varNode);
                if (null == dapType)
                    throw new InvalidProgramException("Unexpected error, unsupported DAP type");
                this.dapURI.AddInternalTypeDetails(i, dapType);
            }
            return true;
        }

        /// <summary>
        /// This method verifies if the DAP URL is correct and then
        /// tries to download the ddx file from the server and loads
        /// it in the dom object. If an error occurs this method
        /// throws an exception.
        /// </summary>
        private void Init()
        {
            if (null == this.dapURI || string.IsNullOrEmpty(this.dapURI.ServerName)
                || string.IsNullOrEmpty(this.dapURI.OpenDAPFile))
                throw new ArgumentException();

            // To get the ddx information appends the .ddx extension at the
            // end of the URL
            string url = (this.dapURI.ServerName + "/" + this.dapURI.OpenDAPFile);
            url += ".ddx";

            // If the url has an error like server name incorrect etc then this
            // GetResponse will throw an exception which will propogate. Thus no
            // exceptions are caught here.
            System.Net.WebRequest webReq = HttpWebRequest.Create(url);
            WebResponse webResp = webReq.GetResponse();
            System.IO.Stream stream = webResp.GetResponseStream();
            this.metaXml.Load(stream);

            webResp.Close();
            // Check the arguments set in the query.
            this.CheckVariables();
            this.VerifySelections();
        }

        /// <summary>
        /// Constructs the URL selection modifier. The format for sub sampling
        /// is varName[startIndex, stride, stopIndex].
        /// The boolean parameter is to avoid appending the variable name to the
        /// begining of the selection string. This is to support the case where
        /// the sampling string is being costructed for the same varible more
        /// than once.
        /// </summary>
        private string ConstructSamplingString(bool useVarName, string varName, ref string[] values)
        {
            // The size of string[] should be either 2 or 3.
            if (2 != values.Length && 3 != values.Length)
                throw new ArgumentException("Incorrect arguments for selection");

            StringBuilder selection = new StringBuilder();
            if (useVarName)
                selection.Append(varName);
            selection.Append("[" + values[0] + ":");
            selection.Append(values.Length == 2 ? "1:" : (values[1] + ":"));
            selection.Append(values.Length == 2 ? (values[1] + "]") : (values[2] + "]"));

            return selection.ToString();
        }

        /// <summary>
        /// This method travereses the variable list and creates a
        /// projection string.
        /// </summary>
        private string CreateSelections()
        {
            return "";
        }

        /// <summary>
        /// This method travereses the variable list and creates a
        /// selection string.
        /// </summary>
        private string CreateProjections()
        {
            StringBuilder projections = new StringBuilder();
            for (int i = 0; i < this.dapURI.VariableCount; ++i)
            {
                string varName; OpenDAPURI.DapVariableType dapType;
                this.dapURI.GetVariableDetails(i, out varName, out dapType);

                List<Selection> selections = dapURI.GetSelection(i);
                if ((null != selections) && (selections.Count > 0) &&
                    (dapType == OpenDAPURI.DapVariableType.Array ||
                    dapType == OpenDAPURI.DapVariableType.Grid))
                {
                    InternalDAPType var = this.dapURI.GetVariableInternalType(i);
                    bool useName = true;

                    // If the variable DAP type if Grid then ignore the first dimension
                    // as that is the combination of the inner array dimensions.
                    int j = (dapType == OpenDAPURI.DapVariableType.Grid ? 1 : 0);
                    for (int sels = 0; j < var.VariableLengths.Length; ++j, ++sels)
                    {
                        // If the projection is sepecified for the array dimension
                        // then use those values else create own values
                        if (sels < selections.Count)
                            projections.Append(ConstructSamplingString(useName, varName,
                                ref selections[sels].values));
                        else
                        {
                            string[] tempValues = new string[] { "1", "1", (var.VariableLengths[j] - 1).ToString() };
                            projections.Append(ConstructSamplingString(useName, varName, ref tempValues));
                        }
                        useName = false;
                    }
                    projections.Append(",");
                }
                else
                {
                    projections.Append(varName + ",");
                }
            }
            // Return the projections after removing the additional , at the end
            return (projections.ToString().Substring(0, projections.Length - 1));
        }

        /// <summary>
        /// This method verifies if the selections added to the OpenDAP Query 
        /// are valid. This method checks if the he type of selection specified is 
        /// supported for the variable type.
        /// </summary>
        private void VerifySelections()
        {
            for (int i = 0; i < dapURI.VariableCount; ++i)
            {
                string varName; OpenDAPURI.DapVariableType type;
                this.dapURI.GetVariableDetails(i, out varName, out type);

                List<Selection> selections = this.dapURI.GetSelection(i);
                if (null == selections)
                    continue;
                foreach (Selection selection in selections)
                {
                    if (!this.IsOperatorValid(type, selection.selection))
                        throw new ArgumentException("Slection Type not possible for DAP type");
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// This method creates the opendap query string using all the
        /// information stored in the dap uri object.
        /// </summary>
        /// <returns></returns>
        public Uri CreateQuery()
        {
            // First create all the projections which are the varialbes
            // to be fetched.
            string projections = CreateProjections();

            // Create all the selections, the conditions to be applied
            // for fetching the data.
            string selections = CreateSelections();

            // Append all the strings to create a URI
            string uriString = dapURI.ServerName + dapURI.OpenDAPFile + ".dods?" + projections + selections;

            return (new Uri(uriString));
        }

        #endregion

        #region Private Members

        private OpenDAPURI dapURI;

        private XmlDocument metaXml;

        private Dictionary<OpenDAPURI.DapVariableType,
            List<OpenDAPURI.DAPOperators>> typeOperatorsMap;

        #endregion
    }
}
